<script setup lang="ts">
  import styles from "./captcha.module.scss";
  import { Device } from "@bidiao/framework";
  import IconRefresh from "./icons/IconRefresh.vue";
  import IconArrow from "./icons/IconArrow.vue";
  import IconArrow2 from "./icons/IconArrow2.vue";
  import IconAirplane from "./icons/IconAirplane.vue";
  import IconAirplane2 from "./icons/IconAirplane2.vue";
  import IconX from "./icons/IconX.vue";
  import IconCheck from "./icons/IconCheck.vue";
  import { CaptchaStatus, CaptchaSideIcon, EVENT_DOWN, EVENT_MOVE, EVENT_UP } from "./models";
  import { gen, validate } from "./apis";
  import Bluebird from "bluebird";

  const props = defineProps({
    // 应用ID
    appId: {
      type: String,
      required: true,
    },
    scheme: {
      type: String,
      default: "Default",
    },
    type: {
      type: Number,
      default: 2,
    },
    // 随机串
    nonce: String,
    // 默认、圆角
    theme: String,
    // 无感验证
    sence: Boolean,
    placeholder: {
      type: String,
      default: "请将滑块拖动到最右边",
    },
    // 拖动图标
    sideIcon: {
      type: Number,
      // default: CaptchaSideIcon.Airplane,
      default: CaptchaSideIcon.Arrow,
    },
  });

  const modelValue = defineModel();
  const emit = defineEmits(["active", "fail", "success"]);

  const sliderBtnElm = ref<HTMLDivElement>(null);
  const sliderProcessElm = ref<HTMLDivElement>(null);
  const rootElm = ref<HTMLDivElement>(null);
  const status = ref(CaptchaStatus.None);
  const btnOffsetX = ref(0);
  // 记录的行为信息
  let track: ReturnType<typeof createTrack>;
  // 验证码加载完成的时间
  let startTime: number;
  const offsetTime = 7708.29696;
  let timeline: number;
  let captchaToken: string;

  const computedBtnStyle = computed(() => {
    if (btnOffsetX.value > 0) {
      if (status.value !== CaptchaStatus.Loading && status.value !== CaptchaStatus.None) {
        return {
          left: `${btnOffsetX.value}px`,
        };
      }
    } else {
      return {};
    }
  });
  const computedProcessStyle = computed(() => {
    if (btnOffsetX.value > 0) {
      if (status.value !== CaptchaStatus.Loading && status.value !== CaptchaStatus.None) {
        return {
          "--process-value": `${btnOffsetX.value}px`,
        };
      }
    } else {
      return {};
    }
  });
  const computedRootClasses = computed(() => {
    const classes = [styles.captcha];
    if (props.theme) {
      classes.push(`slider-bar-${props.theme}`);
    }
    switch (status.value) {
      case CaptchaStatus.Active:
        classes.push("active");
        break;
      case CaptchaStatus.Loading:
        classes.push("loading");
        break;
      case CaptchaStatus.Validing:
        classes.push("validing");
        break;
      case CaptchaStatus.Success:
        classes.push("success");
        break;
      case CaptchaStatus.Fail:
        classes.push("fail");
        break;
      case CaptchaStatus.Complete:
        classes.push("complete");
        break;
    }
    return classes;
  });
  const offsetRatio = computed(() => {
    if (rootElm.value && sliderBtnElm.value) {
      return btnOffsetX.value / (rootElm.value.clientWidth - sliderBtnElm.value.clientWidth);
    } else {
      return 0;
    }
  });

  onMounted(() => {
    timeline = Number(document.timeline.currentTime);
    refreshCaptcha();
    const sliderBtnEl = sliderBtnElm.value;
    if (sliderBtnEl) {
      sliderBtnEl.addEventListener(EVENT_DOWN, onSliderBtnMousedown, false);
    }
  });
  onBeforeUnmount(() => {
    const sliderBtnEl = sliderBtnElm.value;
    if (sliderBtnEl) {
      sliderBtnEl.removeEventListener(EVENT_DOWN, onSliderBtnMousedown);
    }
  });
  defineExpose({
    offsetRatio,
    status: status,
  });

  function onSliderBtnMousedown(e: MouseEvent | TouchEvent) {
    e.stopImmediatePropagation();
    e.stopPropagation();
    e.preventDefault();
    if (status.value !== CaptchaStatus.Ready || sliderBtnElm.value.offsetLeft > 0) {
      return;
    }
    // 判断PC端是否左键点击的
    if (Device.PC && (e as MouseEvent).button !== 0) {
      return;
    }
    // 进行拖动操作时，肯定是从 0 的位置开始的
    const maxOffsetX = rootElm.value.clientWidth - sliderProcessElm.value.clientWidth;

    window.addEventListener(EVENT_MOVE, mousemove, false);
    window.addEventListener(EVENT_UP, mouseup, false);
    document.body.style.userSelect = "none";
    document.body.style.webkitUserSelect = "none";
    status.value = CaptchaStatus.Active;
    emit("active");

    const downX = Device.Mobile ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX;
    const downY = Device.Mobile ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY;

    // 创建本次行为记录对象
    track = createTrack();
    track.push(downX, downY, e.timeStamp);

    function mousemove(e: MouseEvent | TouchEvent) {
      e.stopImmediatePropagation();
      e.stopPropagation();
      e.preventDefault();
      const x = Device.Mobile ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX;
      const y = Device.Mobile ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY;
      track.push(x, y, e.timeStamp);

      let offsetX = x - downX;
      if (offsetX < 0) {
        offsetX = 0;
      } else if (offsetX > maxOffsetX) {
        offsetX = maxOffsetX; // 336 - 40
      }
      btnOffsetX.value = offsetX;
    }
    function mouseup(e) {
      e.stopImmediatePropagation();
      e.stopPropagation();
      e.preventDefault();
      // const x = Device.Mobile ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX;
      // const y = Device.Mobile ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY;
      // track.push(x, y);

      status.value = CaptchaStatus.Validing;

      document.body.style.userSelect = "";
      document.body.style.webkitUserSelect = "";
      window.removeEventListener(EVENT_MOVE, mousemove);
      window.removeEventListener(EVENT_UP, mouseup);
      doBehavorVerify();
    }
  }
  // 行为校验
  async function doBehavorVerify() {
    const delayTime = 2100 - Number(document.timeline.currentTime) + (startTime - timeline) / offsetTime + timeline;
    if (delayTime > 0) {
      await Bluebird.delay(delayTime);
    }

    const completeTime = -Number(document.timeline.currentTime) * offsetTime; // 请求时间转化为秒

    const res = await validate({
      appId: props.appId,
      token: captchaToken,
      trackData: {
        rt: track.rect,
        xy: track.XY,
        es: track.eventStamp,
        sp: track.speeds.reverse(), //  速度反转
      },
      startTime,
      completeTime,
      timeline,
    });
    if (res.status !== 200) {
      status.value = CaptchaStatus.Fail;
      emit("fail");
      setTimeout(resetCpatcha, 1500);
      return;
    }

    const data = await res.json();
    // CaptchaStatus.Success;
    modelValue.value = data.token;
    status.value = CaptchaStatus.Success;
    emit("success");
    setTimeout(() => {
      btnOffsetX.value = 0;
      status.value = CaptchaStatus.Complete;
    }, 750);
  }
  function resetCpatcha() {
    btnOffsetX.value = 0;
    status.value = CaptchaStatus.Ready;
  }
  // 刷新验证码
  async function refreshCaptcha() {
    status.value = CaptchaStatus.Loading;
    startTime = (Number(document.timeline.currentTime) - timeline) * offsetTime + timeline;

    try {
      const res = await gen({ appId: props.appId, scheme: props.scheme, type: props.type });
      const data = await res.json();

      const { token } = data;
      captchaToken = token;
      resetCpatcha();
    } catch {
      status.value = CaptchaStatus.Fail;
    }
  }
  // 创建行为数据对象
  function createTrack() {
    const btnRect = sliderBtnElm.value.getBoundingClientRect();
    const rootRect = rootElm.value.getBoundingClientRect();
    return {
      rect: {
        x: btnRect.x,
        y: btnRect.y,
        width: btnRect.width,
        height: btnRect.height,

        left: rootRect.left,
        top: rootRect.top,
        right: rootRect.right,
        bottom: rootRect.bottom,
      },
      x: [],
      y: [],
      eventStamp: [],
      push(x: number, y: number, eventStamp: number) {
        this.x.push(x);
        this.y.push(y);
        this.eventStamp.push(eventStamp);
      },
      get XY() {
        const firstPoint = {
          x: this.x[0] - (this.rect.x << 0) - this.rect.width / 2,
          y: this.y[0] - (this.rect.y << 0) - this.rect.height / 2,
        };
        return [firstPoint.y, ...this.y.slice(1).map(m => m - this.y[0]), firstPoint.x, ...this.x.slice(1).map(m => m - this.x[0])];
      },
      get speeds() {
        const results = [];
        for (let index = 1, length = this.x.length; index < length; index++) {
          const prev = { x: this.x[index - 1], y: this.y[index - 1], time: this.eventStamp[index - 1] };
          const curr = { x: this.x[index], y: this.y[index], time: this.eventStamp[index] };
          const timeDiff = curr.time - prev.time;
          const distance = Math.sqrt((curr.x - prev.x) ** 2 + (curr.y - prev.y) ** 2);
          const speed = distance / timeDiff;
          results.push(speed);
        }
        return results;
      },
    };
  }
</script>

<template>
  <div ref="rootElm"
       class="slider-bar"
       :class="computedRootClasses">
    <transition>
      <div v-if="status === CaptchaStatus.Loading"
           class="slider-bar--placeholder">安全模块加载中</div>
      <div v-else-if="status === CaptchaStatus.Ready || status === CaptchaStatus.Active"
           class="slider-bar--placeholder"><slot name="placeholder">{{ placeholder }}</slot></div>
      <div v-else-if="status === CaptchaStatus.Validing"
           class="slider-bar--placeholder">安全验证中</div>
      <div v-else-if="status === CaptchaStatus.Fail"
           class="slider-bar--placeholder">验证未通过，请重试！</div>
      <div v-else-if="status === CaptchaStatus.Success || status === CaptchaStatus.Complete"
           class="slider-bar--placeholder">您的验证已通过</div>
    </transition>
    <div ref="sliderProcessElm"
         class="slider-bar--process"
         :style="computedProcessStyle"></div>
    <div ref="sliderBtnElm"
         class="slider-bar--btn"
         :style="computedBtnStyle">
      <icon-refresh v-if="status === CaptchaStatus.Validing || status === CaptchaStatus.Loading"
                    :class="styles.animRotate"></icon-refresh>
      <icon-x v-else-if="status === CaptchaStatus.Fail"
              class="icon-x"></icon-x>
      <icon-check v-else-if="status === CaptchaStatus.Success || status === CaptchaStatus.Complete"
                  class="icon-check" ></icon-check>
      <template v-else-if="status === CaptchaStatus.Ready">
        <icon-airplane2 v-if="sideIcon === CaptchaSideIcon.Airplane"
                        class="icon-airplane"></icon-airplane2>
        <icon-arrow2 v-else
                     class="icon-arrow"></icon-arrow2>
      </template>
      <template v-else-if="status === CaptchaStatus.Active">
        <icon-airplane v-if="sideIcon === CaptchaSideIcon.Airplane"
                       class="icon-airplane"></icon-airplane>
        <icon-arrow v-else
                    class="icon-arrow"></icon-arrow>
      </template>
    </div>
  </div>
</template>

<style lang="scss" scoped>
  // 所有的svg 图标都会影响上一层元素事件的触发，所以需要禁用
  svg {
    pointer-events: none;
  }
  .icon-airplane,
  .icon-arrow {
    // transform: rotate(45deg) translateY(-0.125em);
    transform: rotate(90deg) translateY(-0.125em);
  }
  .icon-check {
    transform: scale(1.35);
  }

  .slider-bar {
    --btn-bg-color: #fff;
    --btn-color: var(--primary-color);
    // --btn-shadow-color: rgba(255, 89, 6, 0.2);
    // --btn-hover-shadow-color: rgba(255, 89, 6, 0.4);
    --btn-shadow-color: color-mix(in oklch, var(--btn-color), transparent 80%);
    --btn-hover-shadow-color: color-mix(in oklch, var(--btn-color), transparent 60%);
    --btn-active-bg-color: var(--btn-color);
    --btn-active-color: var(--btn-bg-color);
    --btn-size: var(--bar-height);
    --btn-radius: var(--bar-radius);

    --bar-height: 40px;
    --bar-radius: 5.25px;

    position: relative;
    height: var(--bar-height);
    line-height: var(--bar-height);
    border-radius: var(--bar-radius);
    background-color: #eee;
    text-align: center;
    transition: all 0.2s ease-in-out;
    &.fail {
      --btn-color: var(--fail-color);
    }
    &.success,
    &.complete {
      --btn-color: var(--success-color);
    }

    &--process {
      --process-value: 0px;
      border-radius: var(--bar-radius);
      position: absolute;
      z-index: 1;
      left: 0;
      top: 0;
      height: 100%;
      opacity: 0.2;
      width: calc(var(--process-value) + var(--btn-size));
      background-color: #ddd;
      transition: all 0.75s ease-out;
    }

    &--btn {
      display: flex;
      align-items: center;
      justify-content: center;
      position: absolute;
      z-index: 5;
      font-size: calc(var(--btn-size) / 2);
      left: 0;
      top: 50%;
      margin-top: -1em;
      width: 2em;
      height: 2em;
      border-radius: var(--btn-radius);
      color: var(--btn-color);
      background-color: var(--btn-bg-color);
      box-shadow: 0 0 4px 0 var(--btn-shadow-color);
      cursor: pointer;
      transition: all 0.75s ease-out;
      transform: rotate(0) scale(0.9);

      &:hover {
        box-shadow: 0 0 10px 0 var(--btn-hover-shadow-color);
      }
    }

    &--placeholder {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
      color: #999;
      font-size: 0.95em;
      touch-action: none;
      -webkit-touch-callout: none;
      user-select: none;
      -webkit-user-select: none; // 苹果识别

      background-image: -webkit-gradient(
        linear,
        left top,
        right top,
        color-stop(0, #999),
        color-stop(0.425, #aaa),
        color-stop(0.5, #333),
        color-stop(0.575, #aaa),
        color-stop(1, #999)
      );
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
      -webkit-text-size-adjust: none;
      -webkit-animation: slidetounlock 2.5s infinite;
      animation: slidetounlock 2.5s infinite;
    }

    // &.complete {
    //   background-color: var(--btn-color);
    // }
    &.complete &--placeholder {
      color: var(--btn-active-bg-color);
    }
    &.complete &--process {
      width: 100%;
    }

    &.active &--process,
    &.validing &--process,
    &.active &--btn,
    &.validing &--btn {
      transition: none;
    }

    &.active &--process,
    &.validing &--process,
    &.fail &--process,
    &.success &--process,
    &.complete &--process {
      background-color: var(--btn-color);
    }

    &.active &--btn,
    &.validing &--btn,
    &.fail &--btn,
    &.success &--btn,
    &.complete &--btn {
      color: var(--btn-active-color);
      background-color: var(--btn-active-bg-color);
      transform: rotate(720deg) scale(0.9);
    }
    &.complete &--btn {
      cursor: default;
      box-shadow: none;
    }

    &-round {
      --bar-radius: 50rem;
      --btn-radius: var(--bar-radius);
    }
  }

  @keyframes slidetounlock {
    0% {
      background-position: -168px 0;
    }
    100% {
      background-position: 168px 0;
    }
  }

  .v-enter-active,
  .v-leave-active {
    transition: all 0.2s ease-in-out;
    transform: scale(1);
  }

  .v-enter-from,
  .v-leave-to {
    transform: scale(0);
  }
</style>
